/**
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE
 */

import { BidiModule, Dir, Direction } from '@angular/cdk/bidi';
import { Component, DebugElement, NgZone, provideZoneChangeDetection, ViewChild } from '@angular/core';
import { ComponentFixture, fakeAsync, flush, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { PRIMARY_OUTLET, provideRouter, Router, RouterOutlet, Routes } from '@angular/router';

import { NzSafeAny } from 'ng-zorro-antd/core/types';
import { provideNzIconsTesting } from 'ng-zorro-antd/icon/testing';

import { NzBreadCrumbItemComponent } from './breadcrumb-item.component';
import { NzBreadCrumbComponent } from './breadcrumb.component';
import { NzBreadCrumbModule } from './breadcrumb.module';
import { NzDemoBreadcrumbBasicComponent } from './demo/basic';
import { NzDemoBreadcrumbDropdownComponent } from './demo/dropdown';
import { NzDemoBreadcrumbSeparatorComponent } from './demo/separator';

describe('breadcrumb', () => {
  describe('basic', () => {
    let fixture: ComponentFixture<NzDemoBreadcrumbBasicComponent>;
    let items: DebugElement[];
    let breadcrumb: DebugElement;

    beforeEach(() => {
      fixture = TestBed.createComponent(NzDemoBreadcrumbBasicComponent);
      items = fixture.debugElement.queryAll(By.directive(NzBreadCrumbItemComponent));
      breadcrumb = fixture.debugElement.query(By.directive(NzBreadCrumbComponent));
    });

    it('should have correct style', () => {
      fixture.detectChanges();
      expect(items.every(item => item.nativeElement.firstElementChild!.classList.contains('ant-breadcrumb-link'))).toBe(
        true
      );
      expect(items.every(item => item.nativeElement.children[1].classList.contains('ant-breadcrumb-separator'))).toBe(
        true
      );
      expect(breadcrumb.nativeElement.classList.contains('ant-breadcrumb')).toBe(true);
    });
  });

  describe('dropdown', () => {
    let fixture: ComponentFixture<NzDemoBreadcrumbDropdownComponent>;
    let items: DebugElement[];

    beforeEach(() => {
      fixture = TestBed.createComponent(NzDemoBreadcrumbDropdownComponent);
      items = fixture.debugElement.queryAll(By.directive(NzBreadCrumbItemComponent));
    });

    it('should dropdown work', () => {
      fixture.detectChanges();

      const dropdownElement = items[2];
      expect((dropdownElement.nativeElement as HTMLElement).querySelector('.ant-dropdown-trigger')).not.toBe(null);
    });
  });

  describe('separator', () => {
    let fixture: ComponentFixture<NzDemoBreadcrumbSeparatorComponent>;
    let items: DebugElement[];
    let breadcrumbs: DebugElement[];

    beforeEach(() => {
      TestBed.configureTestingModule({
        providers: [provideNzIconsTesting()]
      });
    });

    beforeEach(() => {
      fixture = TestBed.createComponent(NzDemoBreadcrumbSeparatorComponent);
      items = fixture.debugElement.queryAll(By.directive(NzBreadCrumbItemComponent));
      breadcrumbs = fixture.debugElement.queryAll(By.directive(NzBreadCrumbComponent));
    });

    it('should nzSeparator work', () => {
      fixture.detectChanges();
      expect(items.every(item => item.nativeElement.firstElementChild!.classList.contains('ant-breadcrumb-link'))).toBe(
        true
      );
      expect(items.every(item => item.nativeElement.children[1].classList.contains('ant-breadcrumb-separator'))).toBe(
        true
      );
      expect(breadcrumbs.every(breadcrumb => breadcrumb.nativeElement.classList.contains('ant-breadcrumb'))).toBe(true);
      expect(items[0].nativeElement.children[1].innerText.indexOf('>') > -1).toBe(true);
      expect(items[3].nativeElement.children[1].firstElementChild!.classList.contains('anticon-arrow-right')).toBe(
        true
      );
    });
  });

  describe('auto generated', () => {
    let fixture: ComponentFixture<NzBreadcrumbAutoGenerateDemoComponent>;
    let router: Router;
    let breadcrumb: DebugElement;

    it('should auto generating work', fakeAsync(() => {
      TestBed.configureTestingModule({
        providers: [provideRouter(routes), provideZoneChangeDetection()]
      });

      fixture = TestBed.createComponent(NzBreadcrumbAutoGenerateDemoComponent);
      breadcrumb = fixture.debugElement.query(By.directive(NzBreadCrumbComponent));

      fixture.ngZone!.run(() => {
        router = TestBed.inject(Router);
        router.initialNavigation();

        // Should generate 2 breadcrumbs when reaching out of the `data` scope.
        router.navigate(['one', 'two', 'three', 'four']);
        flushFixture(fixture);
        expect(breadcrumb.componentInstance.breadcrumbs.length).toBe(2);

        // TODO: pending this test because of Angular's bug: https://github.com/angular/angular/issues/25837
        // const items = fixture.debugElement.queryAll(By.directive(NzBreadCrumbItemComponent));
        // dispatchMouseEvent(items[1].nativeElement.querySelector('a'), 'click');
        // flushFixture(fixture);
        // expect(breadcrumb.componentInstance.breadcrumbs.length).toBe(1);

        // Should generate breadcrumbs correctly.
        router.navigate(['one', 'two', 'three']);
        flushFixture(fixture);
        expect(breadcrumb.componentInstance.breadcrumbs.length).toBe(2);
        router.navigate(['one', 'two']);
        flushFixture(fixture);
        expect(breadcrumb.componentInstance.breadcrumbs.length).toBe(1);

        // Shouldn't generate breadcrumb at all.
        router.navigate(['one']);
        flushFixture(fixture);
        expect(breadcrumb.componentInstance.breadcrumbs.length).toBe(0);

        router.navigate(['/']);
        flushFixture(fixture);
        router.navigate([{ outlets: { nonPrimary: ['one', 'two'] } }]);
        flushFixture(fixture);
        expect(router.url).toBe('/(nonPrimary:one/two)');
      });
    }));

    it('should route data breadcrumb label work', fakeAsync(() => {
      TestBed.configureTestingModule({
        providers: [provideRouter(customRouteLabelRoutes)]
      });

      fixture = TestBed.createComponent(NzBreadcrumbRouteLabelDemoComponent);
      breadcrumb = fixture.debugElement.query(By.directive(NzBreadCrumbComponent));

      fixture.ngZone!.run(() => {
        router = TestBed.inject(Router);
        router.initialNavigation();

        // Should nzRouteLabel value is 'customBreadcrumb'
        flushFixture(fixture);
        expect(breadcrumb.componentInstance.nzRouteLabel).toBe('customBreadcrumb');

        // Should generate 2 breadcrumbs when reaching out of the `data` scope.
        router.navigate(['one', 'two', 'three', 'four']);
        flushFixture(fixture);
        expect(breadcrumb.componentInstance.breadcrumbs.length).toBe(2);
        expect(breadcrumb.componentInstance.breadcrumbs[0].label).toBe('Layer 2');
        expect(breadcrumb.componentInstance.breadcrumbs[1].label).toBe('Layer 3');
      });
    }));

    it('should [nzRouteLabelFn] work', fakeAsync(() => {
      TestBed.configureTestingModule({
        providers: [provideRouter(customRouteLabelRoutes)]
      });

      fixture = TestBed.createComponent(NzBreadcrumbRouteLabelWithCustomFnDemoComponent);
      breadcrumb = fixture.debugElement.query(By.directive(NzBreadCrumbComponent));

      fixture.ngZone!.run(() => {
        router = TestBed.inject(Router);
        router.initialNavigation();

        // Should nzRouteLabel value is 'customBreadcrumb'
        flushFixture(fixture);
        expect(breadcrumb.componentInstance.nzRouteLabel).toBe('customBreadcrumb');

        // Should generate 2 breadcrumbs when reaching out of the `data` scope.
        router.navigate(['one', 'two', 'three', 'four']);
        flushFixture(fixture);
        expect(breadcrumb.componentInstance.breadcrumbs.length).toBe(2);
        expect(breadcrumb.componentInstance.breadcrumbs[0].label).toBe('Layer 2 Layer 2');
        expect(breadcrumb.componentInstance.breadcrumbs[1].label).toBe('Layer 3 Layer 3');
      });
    }));

    it('should [nzRouteFn] work', fakeAsync(() => {
      TestBed.configureTestingModule({
        providers: [provideRouter(customRouteLabelRoutes)]
      });

      fixture = TestBed.createComponent(NzBreadcrumbRouteWithCustomFnDemoComponent);
      breadcrumb = fixture.debugElement.query(By.directive(NzBreadCrumbComponent));

      fixture.ngZone!.run(() => {
        router = TestBed.inject(Router);
        router.initialNavigation();

        // Breadcrumb should contain added params by nzRouteFn
        router.navigate(['one', 'two']);
        flushFixture(fixture);
        expect(breadcrumb.componentInstance.breadcrumbs[0].url).toContain('active=true');
      });
    }));

    it('should route data breadcrumb navigate work', fakeAsync(() => {
      TestBed.configureTestingModule({
        providers: [provideRouter(customRouteLabelRoutes)]
      });

      fixture = TestBed.createComponent(NzBreadcrumbRouteLabelDemoComponent);
      breadcrumb = fixture.debugElement.query(By.directive(NzBreadCrumbComponent));

      fixture.ngZone!.run(() => {
        router = TestBed.inject(Router);
        router.initialNavigation();

        flushFixture(fixture);
        expect(breadcrumb.componentInstance.nzRouteLabel).toBe('customBreadcrumb');

        router.navigate(['one', 'two', 'three', 'four']);
        flushFixture(fixture);

        fixture.debugElement.query(By.css('a')).nativeElement.click();
        flushFixture(fixture);
        expect(router.url).toBe('/one/two');
      });
    }));

    it('should raise error when RouterModule is not included', fakeAsync(() => {
      expect(() => {
        fixture = TestBed.createComponent(NzBreadcrumbAutoGenerateErrorDemoComponent);
        fixture.detectChanges();
      }).toThrowError();
    }));

    it('should call navigate() within the Angular zone', fakeAsync(() => {
      let navigateHasBeenCalledWithinTheAngularZone = false;

      // todo: use zoneless
      TestBed.configureTestingModule({
        providers: [provideRouter(customRouteLabelRoutes), provideZoneChangeDetection()]
      });

      fixture = TestBed.createComponent(NzBreadcrumbRouteLabelDemoComponent);
      breadcrumb = fixture.debugElement.query(By.directive(NzBreadCrumbComponent));

      const navigate = breadcrumb.componentInstance.navigate;
      const spy = spyOn(breadcrumb.componentInstance, 'navigate').and.callFake((url: string, event: MouseEvent) => {
        navigateHasBeenCalledWithinTheAngularZone = NgZone.isInAngularZone();
        return navigate.call(breadcrumb.componentInstance, url, event);
      });

      router = TestBed.inject(Router);
      router.initialNavigation();
      flushFixture(fixture);

      router.navigate(['one', 'two']);
      flushFixture(fixture);

      fixture.debugElement.query(By.css('a')).nativeElement.click();
      flushFixture(fixture);

      expect(spy).toHaveBeenCalledTimes(1);
      expect(navigateHasBeenCalledWithinTheAngularZone).toBeTrue();
    }));
  });

  describe('RTL', () => {
    it('should className correct on dir change', async () => {
      const fixture = TestBed.createComponent(NzTestBreadcrumbRtlComponent);
      const breadcrumb = fixture.debugElement.query(By.directive(NzBreadCrumbComponent));
      await fixture.whenStable();
      expect(breadcrumb.nativeElement.classList).toContain('ant-breadcrumb-rtl');

      fixture.componentInstance.direction = 'ltr';
      fixture.changeDetectorRef.markForCheck();
      await fixture.whenStable();
      expect(breadcrumb.nativeElement.className).not.toContain('ant-breadcrumb-rtl');
    });
  });
});

// eslint-disable-next-line  @typescript-eslint/no-explicit-any
function flushFixture(fixture: ComponentFixture<any>): void {
  fixture.detectChanges();
  flush();
  fixture.detectChanges();
}

@Component({
  imports: [RouterOutlet, NzBreadCrumbModule],
  selector: 'nz-test-breadcrumb',
  template: `
    <nz-breadcrumb [nzAutoGenerate]="true"></nz-breadcrumb>
    <router-outlet></router-outlet>
    <router-outlet name="nonPrimary"></router-outlet>
  `
})
class NzBreadcrumbAutoGenerateDemoComponent {}

@Component({
  imports: [RouterOutlet, NzBreadCrumbModule],
  template: `
    <nz-breadcrumb [nzAutoGenerate]="true" [nzRouteLabel]="'customBreadcrumb'"></nz-breadcrumb>
    <router-outlet></router-outlet>
  `
})
class NzBreadcrumbRouteLabelDemoComponent {}

@Component({
  imports: [RouterOutlet, NzBreadCrumbModule],
  template: `
    <nz-breadcrumb
      [nzAutoGenerate]="true"
      [nzRouteLabel]="'customBreadcrumb'"
      [nzRouteLabelFn]="labelFn"
    ></nz-breadcrumb>
    <router-outlet></router-outlet>
  `
})
class NzBreadcrumbRouteLabelWithCustomFnDemoComponent {
  labelFn = (label: string): string => (label ? `${label} ${label}` : '');
}

@Component({
  imports: [RouterOutlet, NzBreadCrumbModule],
  template: `
    <nz-breadcrumb [nzAutoGenerate]="true" [nzRouteLabel]="'customBreadcrumb'" [nzRouteFn]="routeFn"></nz-breadcrumb>
    <router-outlet></router-outlet>
  `
})
class NzBreadcrumbRouteWithCustomFnDemoComponent {
  routeFn = (route: string): string => `${route};active=true`;
}

@Component({
  imports: [NzBreadCrumbModule],
  template: '<nz-breadcrumb [nzAutoGenerate]="true"></nz-breadcrumb>'
})
class NzBreadcrumbAutoGenerateErrorDemoComponent {}

@Component({
  template: 'empty'
})
class NzBreadcrumbNullComponent {}

const routes: Routes = [
  {
    path: 'one',
    component: NzBreadcrumbAutoGenerateDemoComponent,
    data: {
      breadcrumb: ''
    },
    children: [
      {
        path: 'two',
        component: NzBreadcrumbNullComponent,
        data: {
          breadcrumb: 'Layer 2'
        },
        children: [
          {
            path: 'three',
            component: NzBreadcrumbNullComponent,
            data: {
              breadcrumb: 'Layer 3'
            },
            children: [
              {
                path: 'four',
                component: NzBreadcrumbNullComponent,
                data: {
                  breadcrumb: ''
                }
              }
            ]
          }
        ]
      }
    ]
  },
  // Should only work for the primary outlet.
  {
    path: 'one',
    outlet: 'nonPrimary',
    component: NzBreadcrumbAutoGenerateDemoComponent,
    data: {
      breadcrumb: ''
    },
    children: [
      {
        path: 'two',
        component: NzBreadcrumbNullComponent,
        data: {
          breadcrumb: 'Layer 2'
        }
      }
    ]
  }
];

const customRouteLabelRoutes: Routes = [
  {
    path: 'one',
    component: NzBreadcrumbRouteLabelDemoComponent,
    data: {
      customBreadcrumb: ''
    },
    children: [
      {
        path: 'two',
        component: NzBreadcrumbNullComponent,
        data: {
          customBreadcrumb: 'Layer 2'
        },
        children: [
          {
            path: 'three',
            component: NzBreadcrumbNullComponent,
            data: {
              customBreadcrumb: 'Layer 3'
            },
            children: [
              {
                path: 'four',
                component: NzBreadcrumbNullComponent,
                data: {
                  customBreadcrumb: ''
                }
              }
            ]
          }
        ]
      }
    ]
  }
];

@Component({
  imports: [BidiModule, NzDemoBreadcrumbBasicComponent],
  template: `
    <div [dir]="direction">
      <nz-demo-breadcrumb-basic></nz-demo-breadcrumb-basic>
    </div>
  `
})
export class NzTestBreadcrumbRtlComponent {
  @ViewChild(Dir) dir!: Dir;
  direction: Direction = 'rtl';
}

describe('breadcrumb', () => {
  let fixture: ComponentFixture<NzBreadCrumbComponent>;
  let component: NzBreadCrumbComponent;

  beforeEach(() => {
    // todo: use zoneless
    TestBed.configureTestingModule({
      providers: [provideZoneChangeDetection()]
    });
    fixture = TestBed.createComponent(NzBreadCrumbComponent);
    component = fixture.componentInstance;
  });

  it('should have correct value for nextUrl', () => {
    spyOn(component, 'nzRouteFn');
    const mockRout = {
      children: [
        {
          outlet: PRIMARY_OUTLET,
          snapshot: {
            url: [],
            data: {
              breadcrumb: 'breadcrumb'
            }
          },
          children: []
        }
      ]
    };

    component['getBreadcrumbs'](mockRout as NzSafeAny, 'mockUrl', []);

    expect(component.nzRouteFn).toHaveBeenCalledWith('mockUrl');
  });
});
